/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Microsummarizer.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Myk Melez <myk@mozilla.org> (Original Author)
 *  Simon Bünzli <zeniko@gmail.com>
 *  Asaf Romano <mano@mozilla.com>
 *  Dan Mills <thunder@mozilla.com>
 *  Ryan Flint <rflint@dslr.net>
 *  Atte Kemppilä (ThumbsDown)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */


/*
 * this file is largely based on
 * http://lxr.mozilla.org/mozilla/source/browser/components/microsummaries/src/nsMicrosummaryService.js
 * revision 1.82
 */


var EXPORTED_SYMBOLS = ["ResourceLoader"];

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");


function ResourceLoader(method, uri, headers, loadCallback, errorCallback, timeout)
{
    this.method = method;
    this.uri = uri;
    this.headers = headers;
    this.loadCallback = loadCallback;
    this.errorCallback = errorCallback;
    this.timeout = timeout;
    this.allow = {};
    
    this.loadTimer = null;
    this.isTimeout = false;
    
    this.status = null;
    this.contentType = null;
    this.content = null;
}

ResourceLoader.prototype =
{
    load: function()
    {
        this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
        
        var self = this;
        var loadHandler = {handleEvent: function() { self._onLoad(); } };
        var errorHandler = {handleEvent: function() { self._onError(); } };
        var timeoutHandler = {observe: function() { self._onTimeout(); } };
        
        this.request.QueryInterface(Ci.nsIDOMEventTarget);
        //this.request.addEventListener("progress", progressHandler, false);
        this.request.addEventListener("load", loadHandler, false);
        this.request.addEventListener("error", errorHandler, false);
        
        this.request.QueryInterface(Ci.nsIXMLHttpRequest);
        this.request.open(this.method, this.uri.spec, true);
        
        if (this.headers)
            for (var name in this.headers)
                this.request.setRequestHeader(name, this.headers[name]);
        
        if (this.timeout)
        {
            this.loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
            this.loadTimer.init(timeoutHandler, this.timeout * 1000, Ci.nsITimer.TYPE_ONE_SHOT);
        }
        
        this.request.send(null);
    },
    
    cancel: function()
    {
        if (this.request)
            this.request.abort();
        if (this.loadTimer)
            this.loadTimer.cancel();
        this.destroy();
    },
    
    destroy: function()
    {
        this.method = null;
        this.uri = null;
        this.headers = null;
        this.loadCallback = null;
        this.errorCallback = null;
        this.timeout = null;
        this.loadTimer = null;
        this.request = null;
        this.content = null;
        if (this._iframe)
        {
            if (this._iframe.parentNode)
                this._iframe.parentNode.removeChild(this._iframe);
            this._iframe = null;
        }
    },
    
    _onLoad: function(e)
    {
        if (! this.request)
            return;
        if (this.loadTimer)
            this.loadTimer.cancel();
        
        this.status = this.request.status;
        this.contentType = this.request.channel.contentType;
        
        if ((this.status == 200) && (this.contentType == "text/html"))
            this._parse(this.request.responseText);
        else
            this.errorCallback(this);
    },
    
    _onError: function(e)
    {
        if (! this.request)
            return;
        if (this.loadTimer)
            this.loadTimer.cancel();
        
        this.errorCallback(this);
    },
    
    _onTimeout: function(e)
    {
        if (! this.request)
            return;
        
        this.request.abort();
        this.isTimeout = true;
        this.errorCallback(this);
    },
    
    /**
     * Parse a string of HTML text.  Used by _load() when it retrieves HTML.
     * We do this via hidden XUL iframes, which according to bz is the best way
     * to do it currently, since bug 102699 is hard to fix.
     * 
     * @param   htmlText
     *          a string containing the HTML content
     */
    _parse: function(htmlText)
    {
        // Find a window to stick our hidden iframe into.
        var window = Services.wm.getMostRecentWindow("navigator:browser");
        // XXX We can use other windows, too, so perhaps we should try to get
        // some other window if there's no browser window open.  Perhaps we should
        // even prefer other windows, since there's less chance of any browser
        // window machinery like throbbers treating our load like one initiated
        // by the user.
        if (!window) {
            this._onError(event);
            return;
        }
        var document = window.document;
        var rootElement = document.documentElement;
        
        // Create an iframe, make it hidden, and secure it against untrusted content.
        this._iframe = document.createElement('iframe');
        this._iframe.setAttribute("collapsed", true);
        this._iframe.setAttribute("type", "content");
        
        // Insert the iframe into the window, creating the doc shell.
        rootElement.appendChild(this._iframe);
        
        // When we insert the iframe into the window, it immediately starts loading
        // about:blank, which we don't need and could even hurt us (for example
        // by triggering bugs like bug 344305), so cancel that load.
        var webNav = this._iframe.docShell.QueryInterface(Ci.nsIWebNavigation);
        webNav.stop(Ci.nsIWebNavigation.STOP_NETWORK);
        
        // Turn off JavaScript and auth dialogs for security and other things
        // to reduce network load.
        // XXX We should also turn off CSS.
        var allow = this.allow;
        this._iframe.docShell.allowJavascript    = ("javascript"    in allow ? allow.javascript    : false);
        this._iframe.docShell.allowAuth          = ("auth"          in allow ? allow.auth          : false);
        this._iframe.docShell.allowPlugins       = ("plugins"       in allow ? allow.plugins       : false);
        this._iframe.docShell.allowMetaRedirects = ("metaRedirects" in allow ? allow.metaRedirects : false);
        this._iframe.docShell.allowSubframes     = ("subframes"     in allow ? allow.subframes     : true);
        this._iframe.docShell.allowImages        = ("images"        in allow ? allow.images        : false);
        
        var parseHandler = {
            _self: this,
            handleEvent: function MSR_parseHandler_handleEvent(event) {
                event.target.removeEventListener("DOMContentLoaded", this, false);
                try     { this._self._handleParse(event) }
                finally { this._self = null }
            }
        };
        
        // Convert the HTML text into an input stream.
        var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
        converter.charset = "UTF-8";
        var stream = converter.convertToInputStream(htmlText);
        
        // Set up a channel to load the input stream.
        var channel = Cc["@mozilla.org/network/input-stream-channel;1"].createInstance(Ci.nsIInputStreamChannel);
        channel.setURI(this.uri);
        channel.contentStream = stream;
        
        // Load in the background so we don't trigger web progress listeners.
        var request = channel.QueryInterface(Ci.nsIRequest);
        request.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND;
        
        // Specify the content type since we're not loading content from a server,
        // so it won't get specified for us, and if we don't specify it ourselves,
        // then Firefox will prompt the user to download content of "unknown type".
        var baseChannel = channel.QueryInterface(Ci.nsIChannel);
        baseChannel.contentType = "text/html";
        
        // Load as UTF-8, which it'll always be, because XMLHttpRequest converts
        // the text (i.e. XMLHTTPRequest.responseText) from its original charset
        // to UTF-16, then the string input stream component converts it to UTF-8.
        baseChannel.contentCharset = "UTF-8";
        
        // Register the parse handler as a load event listener and start the load.
        // Listen for "DOMContentLoaded" instead of "load" because background loads
        // don't fire "load" events.
        this._iframe.addEventListener("DOMContentLoaded", parseHandler, true);
        var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
        uriLoader.openURI(channel, true, this._iframe.docShell);
    },
    
    /**
     * Handle a load event for the iframe-based parser.
     * 
     * @param event the event object representing the load event
     */
    _handleParse: function(event)
    {
        // XXX Make sure the parse was successful?
        
        this.content = this._iframe.contentDocument;
        this.contentType = this._iframe.contentDocument.contentType;
        this.loadCallback(this);
    }
}
